package IOSreamAndFile;
/*               ByteArrayInputStream
 * InputStream   FileInputStream         BufferedInputStream...
 *               FilterInputStream
 *               ....
 *
 *
 *               ByteArrayOutputStream
 * OutputStream  FileOutputStream        BufferedOutputStream...
 *               FilterOutputStream
 *               ....
 *
 *
 * OutputStream和InputStream类中提供的write和read方法都是抽象方法，需要子类实现
 * 因此，这两个类不能直接创建对象，只能通过子类来创建对象
 * 子类都有这个方法，所以可以通过子类来调用这个方法
 * */

import java.io.*;

public class byteStream {
    public static void main(String[] args) {
        /*字节流：以字节为单位，读写数据的流
        字节流使用FileInputStream/FileOutputStream类构造数据转输通道
        利用OutputStream/InputStream类的方法进行数据的读写
         注意：FileInputStream是InputStream的子类，FileOutputStream是OutputStream的子类
         */
         /*
         FileOutputStream类构造方法：
        //根据提供的文件路径构建一条文件输出通道
        public FileOutputStream(String name) throws FileNotFoundException;
        //根据提供的文件路径构建一条文件输出通道，并根据append的值决定是将内容追加到末尾还是直接覆盖
        public FileOutputStream(String name, boolean append) throws FileNotFoundException;
        //根据提供的文件信息构建一条文件输出通道
        public FileOutputStream(File file) throws FileNotFoundException;
        //根据提供的文件信息构建一条文件输出通道，并根据append的值决定是将内容追加到末尾还是直接覆盖
        public FileOutputStream(File file, boolean append) throws FileNotFoundException;


        OutputStream类的方法：
        public abstract void write(int b) throws IOException;; //写一个字节
        public void write(byte b[]) throws IOException; //将给定的字节数组内容全部写入文件中
        public void write(byte b[], int off, int len) throws IOException;//将给定的字节数组中指定的偏移量和长度之间的内容写入文件中
        public void close() throws IOException;//关闭通道
        */

        //将“Hello World”写入到io.txt文件中
        try {
            File dir = new File("selfStudy\\heiMa\\middleGrammar\\src\\IOSreamAndFile");
            if (!dir.exists()) dir.mkdirs();
            File file = new File(dir, "io.txt");
            if (!file.exists()) file.createNewFile();
            //创建文件输出流对象
            FileOutputStream fos = new FileOutputStream(file, false);//默认是false，覆盖,true是追加
            String text = "你好\n世界";
            byte[] data = text.getBytes();
            //一次写一个字节
            //for(byte b:data) fos.write(b);
            //一次写一个字节数组(常用推荐)
            //fos.write(data);
            //一次写一个字节数组的一部分
            fos.write(data, 3, data.length - 3);//使用偏移量和长度的时候需要考虑数组下标越界
            /*编码小知识
            JDK18开始默认使用UTF-8编码，一个中文占三个字节
            (更低JDK版本使用GBK编码，一个中文占两个字节(Windows系统默认也使用GBK编码))
            如果一次读入字节数不是三个字节的整数倍，会出现乱码
            或者因为使用不同的编码方式解码，也会出现乱码，UTF-8解码时不能用GBK编码解码

            Unicode是字符集，规定了世界所有字符的统一编码规则，为每种语言的每个字符设定唯一编号
            UTF（Unicode Transformation Format，Unicode转换格式 ）是Unicode的实现方式
            是将Unicode字符编号转换为字节序列的具体编码方案
            常见的UTF编码有UTF - 8、UTF - 16、UTF - 32
             */
            fos.close();//一定要手动关闭流通道，不然会一直占用系统资源
        } catch (Exception e) {
            e.printStackTrace();
        }


        /*
        FileInputStream类构造方法：
        //根据提供的文件路径构建一条文件输入通道
        public FileInputStream(String name) throws FileNotFoundException;
        //根据提供的文件信息构建一条文件输入通道
        public FileInputStream(File file) throws FileNotFoundException;

        InputStream类的方法：
        public abstract int read() throws IOException; //读取一个字节
        public int read(byte b[]) throws IOException; //读取多个字节存储至给定的字节数组中
        //读取多个字节按照给定的偏移量及长度存储在给定的字节数组中
        public int read(byte b[], int off, int len) throws IOException;
        public void close() throws IOException;//关闭流，也就是关闭磁盘和内存之间的通道
        public int available() throws IOException;//获取通道中数据的长度
         */
        //将io.txt文件中的内容读取到控制台
        try {
            FileInputStream fis = new FileInputStream("selfStudy\\heiMa\\middleGrammar\\src\\IOSreamAndFile\\io.txt");
            int len = fis.available();
            byte[] data = new byte[len];//开一个文件大小的数组，存放读出的内容
            //一次读一个字节
//           int index = 0;
//           while (true){
//           //读取通道中的数据，一次读取一个字节。如果读取到末尾，则返回-1
//           byte b = (byte) fis.read();
//           //对于读一个字节的方法返回值是abstract int,但是我们知道它的返回值是一个字节，
//           // 所以可以强转为byte，这样就可以直接使用了
//           if(b == -1) break;//如果读取到末尾，read()方法返回-1
//           data[index++] = b;
//           }
//           System.out.println(new String(data));

            int readCount = fis.read(data); //将通道中的数据全部读取到data数组中并返回读取的字节数
            System.out.println("读取了" + readCount + "个字节");
            System.out.println(new String(data));

            fis.close();//关闭通道
        } catch (Exception e) {
            e.printStackTrace();
        }

        /*对于大文件的读入怎么办？
         int len = fis.available();
         byte[] data = new byte[len];
         我不能开一个文件那么大的数组吧
         不用担心，我们可以使用循环，每次读取一个字节数组
         */
        try {
            FileInputStream fis = new FileInputStream("selfStudy\\heiMa\\middleGrammar\\src\\IOSreamAndFile\\io.txt");
            //实际开发中我们一般开1024的整数倍的字节数组
            byte[] data = new byte[1024];
            while (true) {
                int len = fis.read(data); //无论是读还是写，文件指针的位置由流自动维护，无需手动跟踪
                if (len == -1) break;
                //System.out.println(new String(data));
                System.out.println(new String(data,0,len));
                //String的构造方法可以指定偏移量和长度
                //如果不指定长度，会默认从偏移量开始到数组末尾
                //对于byte数组，没有赋值的部分，默认是0，而0在UTF-8属于不可见字符
                //虽然不会输出显示但确实存在的占用空间，所以养成良好的习惯，指定长度
            }
            fis.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


        //用字节流复制文件
        String sourcePath = "selfStudy\\heiMa\\middleGrammar\\src\\IOSreamAndFile\\io.txt";
        String targetPath = "selfStudy\\heiMa\\middleGrammar\\src\\IOSreamAndFile\\ioCopy.txt";
        copyFile(sourcePath, targetPath);
    }//main方法结束

    public static void copyFile(String sourcePath, String targetPath) {
        File file = new File(targetPath);
        File parentFile = file.getParentFile();
        if (!parentFile.exists()) parentFile.mkdirs();

        /*try(){} catch{} JDk17开始支持
         写在括号中的代码只能够是实现了AutoClosable接口的类
         当try(){}代码块执行完毕后，会自动调用实现了AutoClosable接口的类的close()方法
         所以我们可以利用这个特性，将流对象写在try(){}中，这样就可以自动关闭流了
         这样写的好处是，不用手动关闭流，也不用考虑流是否为空，也不用考虑流是否关闭了
         */
        try (FileInputStream fis = new FileInputStream(sourcePath);
             FileOutputStream fos = new FileOutputStream(targetPath,false)) {
            if (!file.exists()) file.createNewFile();//如果文件不存在，就创建文件
            byte[] buffer= new byte[1024];
            while (true){
                int len = fis.read(buffer);
                if(len == -1) break;
                fos.write(buffer,0,len);
                //不能写 fos.write(buffer); 这样会把整个buffer数组都写进去
                //对于其他有空间的地方存进的都是"NULL"这个字符
            }

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}//class结束


class bufferByteStream{
    public static void main(String[] args) {
        /*每次使用write()写入或者read()读取数据，都会直接和磁盘交互，效率非常低
        所以我们可以使用缓冲区，一次性读写多个字节，这样就可以减少磁盘交互的次数
        所以有BufferedInputStream/BufferedOutputStream来实现缓冲区
        其底层开了一个默认大小为1024*9的字节数组作为缓冲区
        此时的read()和write()方法数据会先放到这个缓冲区中
        要么缓冲区满，自动与磁盘交互一次
        要么调用flush()方法与磁盘交互一次
        正常情况下，我们不需要手动调用flush()方法，因为缓冲区满了，会自动与磁盘交互一次
        但是如果是最后一次写入数据，缓冲区可能没有满，所以需要在最后手动调用flush()方法



        BufferedOutputStream构造方法：
        //根据给定的字节输出流创建一个缓冲输出流，缓冲区大小使用默认大小
        public BufferedOutputStream(OutputStream out);
        //根据给定的字节输出流创建一个缓冲输出流，并指定缓冲区大小
        public BufferedOutputStream(OutputStream out, int size);

        BufferedInputStream构造方法：
        //根据给定的字节输入流创建一个缓冲输入流，缓冲区大小使用默认大小
        public BufferedInputStream(InputStream in);
        //根据给定的字节输入流创建一个缓冲输入流，并指定缓冲区大小
        public BufferedInputStream(InputStream in, int size);

         */

        //使用缓冲流复制文件
        String sourcePath = "selfStudy\\heiMa\\middleGrammar\\src\\IOSreamAndFile\\io.txt";
        String targetPath = "selfStudy\\heiMa\\middleGrammar\\src\\IOSreamAndFile\\ioCopy.txt";
        copyFile(sourcePath, targetPath);

    }
    public static void copyFile(String sourcePath, String targetPath) {
        File file = new File(targetPath);
        File parentFile = file.getParentFile();
        if (!parentFile.exists()) parentFile.mkdirs();
        //注意：他们的构造时候需要传入的是字节流对象，而不是文件对象
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourcePath));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(targetPath,false))) {
            byte[] buffer= new byte[1024];
            while (true) {
                int len = bis.read(buffer);
                if (len == -1) break;
                bos.write(buffer, 0, len);
            }
            bos.flush();//防止最后一次写入数据，缓冲区没有满，导致最后一次写入数据没有与磁盘交互
             } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}